import utils from '../utils'
import Common from '../common.js'
import shpconverterTopology from './index.js'
import {ArcCollection} from '../paths'

// @arcs ArcCollection
// @filter Optional filter function, arcIds that return false are excluded
//
function NodeCollection(arcs, filter) {
  if (utils.isArray(arcs)) {
    arcs = new ArcCollection(arcs)
  }
  const arcData = arcs.getVertexData()
  const nn = arcData.nn
  const xx = arcData.xx
  const yy = arcData.yy
  let nodeData = null

  // Accessor function for arcs
  Object.defineProperty(this, 'arcs', {value: arcs})

  const toArray = this.toArray = function() {
    const chains = getNodeChains()
    const flags = new Uint8Array(chains.length)
    const arr = []
    utils.forEach(chains, function(nextIdx, thisIdx) {
      if (flags[thisIdx] == 1) return
      const p = getEndpoint(thisIdx)
      if (!p) return // endpoints of an excluded arc
      const node = {coordinates: p, arcs: []}
      arr.push(node)
      while (flags[thisIdx] != 1) {
        node.arcs.push(chainToArcId(thisIdx))
        flags[thisIdx] = 1
        thisIdx = chains[thisIdx]
      }
    })
    return arr
  }

  this.size = function() {
    return this.toArray().length
  }

  this.findDanglingEndpoints = function() {
    const chains = getNodeChains()
    const arr = []
    for (let i=0, n=chains.length; i<n; i++) {
      if (chains[i] != i) continue // endpoint attaches to a node
      const p = getEndpoint(i)
      if (!p) continue // endpoint belongs to an excluded arc
      arr.push({
        point: p,
        arc: chainToArcId(i)
      })
    }
    return arr
  }

  this.detachAcyclicArcs = function() {
    const chains = getNodeChains()
    let count = 0
    for (let i=0, n=chains.length; i<n; i+= 2) {
      const fwd = i == chains[i]
      const rev = i + 1 == chains[i + 1]
      // detach arcs that are disconnected at one end or the other
      if ((fwd || rev) && !linkIsDetached(i)) {
        this.detachArc(chainToArcId(i))
        count++
      }
    }
    if (count > 0) {
      // removing one acyclic arc could expose another -- need another pass
      count += this.detachAcyclicArcs()
    }
    return count
  }

  this.detachArc = function(arcId) {
    unlinkDirectedArc(arcId)
    unlinkDirectedArc(~arcId)
  }

  this.forEachConnectedArc = function(arcId, cb) {
    let nextId = nextConnectedArc(arcId)
    let i = 0
    while (nextId != arcId) {
      cb(nextId, i++);
      nextId = nextConnectedArc(nextId)
    }
  }

  // Receives an arc id for an arc that enters a node.
  // Returns an array of ids of all other arcs that are connected to the same node.
  //    Returned ids lead into the node (as opposed to outwards from it)
  // An optional filter function receives the directed id (positive or negative)
  //    of each connected arc and excludes arcs for which the filter returns false.
  //    The filter is also applied to the initial arc; if false, no arcs are returned.
  //
  this.getConnectedArcs = function(arcId, filter) {
    const ids = []
    const filtered = !!filter
    let nextId = nextConnectedArc(arcId)
    if (filtered && !filter(arcId)) {
      // return ids;
    }
    while (nextId != arcId) {
      if (!filtered || filter(nextId)) {
        ids.push(nextId)
      }
      nextId = nextConnectedArc(nextId)
    }
    return ids
  }

  // Returns the id of the first identical arc or @arcId if none found
  // TODO: find a better function name
  this.findDuplicateArc = function(arcId) {
    let nextId = nextConnectedArc(arcId)
    let match = arcId
    while (nextId != arcId) {
      if (testArcMatch(arcId, nextId)) {
        if (Common.absArcId(nextId) < Common.absArcId(match)) match = nextId
      }
      nextId = nextConnectedArc(nextId)
    }
    return match
  }

  // returns null if link has been removed from node collection
  function getEndpoint(chainId) {
    return linkIsDetached(chainId) ? null : [nodeData.xx[chainId], nodeData.yy[chainId]]
  }

  function linkIsDetached(chainId) {
    return isNaN(nodeData.xx[chainId])
  }

  function unlinkDirectedArc(arcId) {
    const chainId = arcToChainId(arcId)
    const chains = getNodeChains()
    const nextId = chains[chainId]
    const prevId = prevChainId(chainId)
    nodeData.xx[chainId] = NaN
    nodeData.yy[chainId] = NaN
    chains[chainId] = chainId
    chains[prevId] = nextId
  }

  function chainToArcId(chainId) {
    const absId = chainId >> 1
    return chainId & 1 == 1 ? absId : ~absId
  }

  function arcToChainId(arcId) {
    const fw = arcId >= 0
    return fw ? arcId * 2 + 1 : (~arcId) * 2 // if fw, use end, if rev, use start
  }

  function getNodeChains() {
    if (!nodeData) {
      nodeData = findNodeTopology(arcs, filter)
      if (nn.length * 2 != nodeData.chains.length) utils.error("[NodeCollection] count error")
    }
    return nodeData.chains
  }

  function testArcMatch(a, b) {
    const absA = a >= 0 ? a : ~a
    const absB = b >= 0 ? b : ~b
    const lenA = nn[absA]
    if (lenA < 2) {
      // Don't throw error on collapsed arcs -- assume they will be handled
      //   appropriately downstream.
      // error("[testArcMatch() defective arc; len:", lenA);
      return false
    }
    if (lenA != nn[absB]) return false
    if (testVertexMatch(a, b, -1) &&
        testVertexMatch(a, b, 1) &&
        testVertexMatch(a, b, -2)) {
      return true
    }
    return false
  }

  function testVertexMatch(a, b, i) {
    const ai = arcs.indexOfVertex(a, i)
    const bi = arcs.indexOfVertex(b, i)
    return xx[ai] == xx[bi] && yy[ai] == yy[bi]
  }

  // return arcId of next arc in the chain, pointed towards the shared vertex
  function nextConnectedArc(arcId) {
    const chainId = arcToChainId(arcId)
    const chains =  getNodeChains()
    const nextChainId = chains[chainId]
    if (!(nextChainId >= 0 && nextChainId < chains.length)) utils.error("out-of-range chain id")
    return chainToArcId(nextChainId)
  }

  function prevChainId(chainId) {
    const chains = getNodeChains()
    let prevId = chainId
    let nextId = chains[chainId]
    while (nextId != chainId) {
      prevId = nextId
      nextId = chains[nextId]
      if (nextId == prevId) utils.error("Node indexing error")
    }
    return prevId
  }

  // expose functions for testing
  // this.internal = {
  //   testArcMatch: testArcMatch,
  //   testVertexMatch: testVertexMatch
  // }
}
function findNodeTopology(arcs, filter) {
  const n = arcs.size() * 2
  const xx2 = new Float64Array(n)
  const yy2 = new Float64Array(n)
  const ids2 = new Int32Array(n)

  arcs.forEach2(function(i, n, xx, yy, zz, arcId) {
    const start = i
    const end = i + n - 1
    const start2 = arcId * 2
    const end2 = start2 + 1
    let ax = xx[start]
    let ay = yy[start]
    let bx = xx[end]
    let by = yy[end]
    if (filter && !filter(arcId)) {
      ax = ay = bx = by = NaN
    }

    xx2[start2] = ax
    yy2[start2] = ay
    ids2[start2] = arcId
    xx2[end2] = bx
    yy2[end2] = by
    ids2[end2] = arcId
  })

  const chains = shpconverterTopology.initPointChains(xx2, yy2)
  return {
    xx: xx2,
    yy: yy2,
    ids: ids2,
    chains: chains
  }
}
export default NodeCollection